Skip to content

Entity Framework 中 DateTime 時區問題與解決方案

TLDR

  • DateTimeKind 屬性若為 Unspecified,在進行時區轉換時會產生非預期的偏差。
  • 資料庫欄位(如 datetime2)不儲存時區資訊,導致 Entity Framework 取出資料時 Kind 預設為 Unspecified,進而導致前端顯示時間錯誤。
  • 解決方案為使用 ValueConverter,在寫入資料庫前確保時間為 UTC,並在讀取時強制將 Kind 標記為 Utc
  • 推薦使用 DbContext.ConfigureConventions() 統一設定,避免手動為每個屬性定義轉換器。

DateTime 的時區格式問題

在 .NET 中,DateTimeKind 屬性決定了時間處理的行為。當 KindUnspecified 時,呼叫 ToLocalTime()ToUniversalTime() 會導致系統根據當前環境進行錯誤的時區偏移計算。

什麼情況下會遇到這個問題: 當開發者直接使用 DateTime 物件,且未明確指定或轉換其 Kind 屬性,導致系統在進行時間運算時誤判時區。

以下為 Kind 對轉換行為的影響:

  • Local:呼叫 ToLocalTime() 不會改變時間。
  • Utc:呼叫 ToUniversalTime() 不會改變時間。
  • Unspecified:呼叫 ToLocalTime() 會假設原時間為 UTC 並加上時區偏移;呼叫 ToUniversalTime() 則假設原時間為本機時間並減去偏移。

TIP

為避免此類問題,可參考 ABP.IO 框架的 IClock 實作,透過比對 Kind 來進行標準化處理。

Entity Framework 使用 DateTime 的時區問題

當使用不包含時區資訊的資料庫型別(如 datetimedatetime2)時,Entity Framework 取出的資料其 Kind 永遠為 Unspecified,這導致序列化傳給前端時,時間字串缺少代表 UTC 的 Z 結尾,造成前端顯示的時間與預期有 8 小時誤差。

什麼情況下會遇到這個問題: 當專案採用 Code First 或反向工程,且資料庫欄位未儲存時區資訊,導致 EF Core 讀取資料後,物件的 Kind 屬性無法正確反映 UTC 狀態。

解決方案:使用 ValueConverter

透過 ValueConverter,可以在資料寫入時強制轉換為 UTC,並在讀取時強制標記為 Utc

1. 定義轉換器

csharp
public class UtcDateTimeValueConverter : ValueConverter<DateTime, DateTime> {
    public UtcDateTimeValueConverter()
        : base(v => ToDb(v), v => FromDb(v)) {
    }

    private static DateTime ToDb(DateTime dateTime) {
        return dateTime.Kind == DateTimeKind.Utc ? dateTime : dateTime.ToUniversalTime();
    }

    private static DateTime FromDb(DateTime dateTime) {
        return DateTime.SpecifyKind(dateTime, DateTimeKind.Utc);
    }
}

2. 全域設定(推薦)

在 .NET 6 以上版本,建議使用 ConfigureConventions 統一處理所有 DateTime 欄位,無需逐一設定:

csharp
public partial class MyDbContext : DbContext {
    protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) {
        ArgumentNullException.ThrowIfNull(configurationBuilder);

        configurationBuilder.Properties<DateTime>().HaveConversion<UtcDateTimeValueConverter>();
    }
}

若使用反向工程產生的 DbContext,可利用 OnModelCreatingPartial 方法進行擴充:

csharp
partial void OnModelCreatingPartial(ModelBuilder modelBuilder) {
    foreach (IMutableEntityType entityType in modelBuilder.Model.GetEntityTypes()) {
        foreach (IMutableProperty property in entityType.GetProperties()) {
            if (property.ClrType == typeof(DateTime) || property.ClrType == typeof(DateTime?)) {
                property.SetValueConverter(typeof(UtcDateTimeValueConverter));
            }
        }
    }
}

TIP

本篇的完整可執行範例:CloudyWing/EfCoreBehaviorSample

異動歷程

    • 初版文件建立。
    • 補上對應 GitHub 範例專案連結。